Skip to content

feat: implement state persistence#177

Open
35C4n0r wants to merge 23 commits intomainfrom
35C4n0r/agentapi-state-persistence
Open

feat: implement state persistence#177
35C4n0r wants to merge 23 commits intomainfrom
35C4n0r/agentapi-state-persistence

Conversation

@35C4n0r
Copy link
Collaborator

@35C4n0r 35C4n0r commented Jan 31, 2026

Closes: coder/internal#1256

MergeAfter: #172

@35C4n0r 35C4n0r self-assigned this Feb 1, 2026
@35C4n0r 35C4n0r marked this pull request as ready for review February 1, 2026 15:52
@35C4n0r 35C4n0r marked this pull request as draft February 1, 2026 15:52
@35C4n0r 35C4n0r changed the base branch from main to cj/refactor-conversation-orig February 3, 2026 08:51
@35C4n0r 35C4n0r marked this pull request as ready for review February 3, 2026 08:54
@35C4n0r 35C4n0r marked this pull request as draft February 3, 2026 08:54
@35C4n0r 35C4n0r closed this Feb 3, 2026
@35C4n0r 35C4n0r reopened this Feb 3, 2026
@github-actions
Copy link

github-actions bot commented Feb 3, 2026

✅ Preview binaries are ready!

To test with modules: agentapi_version = "agentapi_177" or download from: https://github.com/coder/agentapi/releases/tag/agentapi_177

@mafredri mafredri self-requested a review February 3, 2026 13:09
Copy link
Member

@mafredri mafredri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work so far! I left a few comments and suggestions. Mainly about moving some concerns from httpapi to cmd/server. I think it would be helpful to have some tests based on real agent session restoration output to evaluate the screentracker changes.

currentStatus = st.ConversationStatusChanging
s.logger.Info("Initial prompt sent successfully")
if !s.stateLoadComplete && s.statePersistenceCfg.LoadState {
_, _ = s.conversation.LoadState(s.statePersistenceCfg.StateFile)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not objecting, just curious. Why do we wait for stability to load the state?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is by design. We wait for the initial stable state to capture a baseline screen snapshot. This baseline allows us to clear any agent-generated messages or screen content before loading our saved state.

func (s *Server) HandleSignals(ctx context.Context, process *termexec.Process) {
// Handle shutdown signals (SIGTERM, SIGINT only on Windows)
shutdownCh := make(chan os.Signal, 1)
signal.Notify(shutdownCh, os.Interrupt, syscall.SIGTERM)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this compile on Windows? IIRC we can only support os.Interrupt there.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

compiles - yes (since PR Preview Build / Build Release Binaries (pull_request) passes), but haven't tested it.

@mafredri mafredri requested a review from SasSwart February 10, 2026 13:38
# Conflicts:
#	cmd/server/server.go
#	lib/httpapi/server.go
#	lib/screentracker/conversation.go
#	lib/screentracker/pty_conversation.go
#	lib/screentracker/pty_conversation_test.go
# Conflicts:
#	lib/httpapi/server.go
#	lib/screentracker/pty_conversation.go
@35C4n0r 35C4n0r marked this pull request as ready for review February 17, 2026 09:43
@35C4n0r 35C4n0r requested a review from mafredri February 17, 2026 10:48
Copy link
Member

@mafredri mafredri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test coverage seems good, but I would really like to see real testdata created for e.g. Claude where all output from the initial conversation, and then again for the restoration part. And testing that AgentAPI does in fact handle this correctly.

I'm not sure how feasible it is, but ideally it'd be nice to capture everything, including control characters, that Claude outputs. Think asciinema recording.

Also, I think we need to really consider everything that can affect the AI agent output. For instance, given the nature of adjustScreenAfterStateLoad, what if --term-height and --term-width have been adjusted between invocations? If the state is being restored, we should probably do so much earlier (in runServer) and print warnings that these options are being overridden by the state restoration and forcing the previous options. Wdyt?

// Store the first stable snapshot for filtering later
snapshots := c.snapshotBuffer.GetAll()
if len(snapshots) > 0 {
c.firstStableSnapshot = c.cfg.FormatMessage(strings.TrimSpace(snapshots[len(snapshots)-1].screen), "")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose we need a FormatMessage nil check here?

Also, what if FormatMessage function has changed between session save and restore (different AgentAPI versions). Potential issue?

Content: agentState.InitialPrompt,
Alias: "",
Hidden: false,
}}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably fine, but asking. Is there a chance that AgentAPI is started with a "new" initial prompt on next start? Should we take that into account instead of overwriting this one?

Do we need to track original prompt separately or can we just discard it from the state? Is it an important component?


// Before the first user message after loading state, return the last message from the loaded state.
// This prevents computing incorrect diffs from the restored screen, as the agent's message should
// remain stable until the user continues the conversation.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Asked elsewhere too, but can a "new" initial prompt be provided on startup and does that interfere with this logic?

@mafredri mafredri requested a review from johnstcn February 18, 2026 09:45
return xerrors.Errorf("failed to unmarshal state (corrupted or invalid JSON): %w", err)
}

//c.cfg.initialPromptSent = agentState.InitialPromptSent
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: remove commented code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AgentAPI: State persistence

3 participants

Comments